/*
 *  linux/kernel/hd.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * This is the low-level hd interrupt support. It traverses the
 * request-list, using interrupts to jump between functions. As
 * all the functions are called within interrupts, we may not
 * sleep. Special care is recommended.
 * 
 *  modified by Drew Eckhardt to check nr of hd's from the CMOS.
 */

#include <linux/config.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/hdreg.h>
#include <asm/system.h>
#include <asm/io.h>
#include <asm/segment.h>

#define MAJOR_NR 3
#include "blk.h"

#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})

/*
关于本程序涉及到的有关复位、重新校正等操作：

reset_controller：复位硬盘控制器（重新设置控制寄存器）
recalibrate：校正一下磁头位置，即磁头归位（WIN_RESTORE命令）
reset_hd：复位硬盘，分两步：
    1、复位硬盘控制器（reset_controller）
    2、建立驱动器参数（WIN_SPECIFY命令）

*/

/* Max read/write errors/sector */
#define MAX_ERRORS	7  // 读/写一个扇区时允许的最多出错次数。
#define MAX_HD		2  // 系统支持的最多硬盘数。

static void recal_intr(void);

// 重新校正标志。当设置了该标志，想控制器发送 WIN_RESTORE 指令，以将磁头移动到0柱面。
static int recalibrate = 0;
// 复位标志。当发生读写错误时会设置该标志并调用相关复位函数（在do_hd_request中调用），以复位硬盘和控制器。
static int reset = 0;

/*
 *  This struct defines the HD's and their types.
 */
// 硬盘信息结构 (Harddisk information struct)
// 各字段分别是 磁头数、每磁道扇区数、柱面数、写前预补偿柱面号、磁头着陆区（停止）柱面号、 控制字节。
struct hd_i_struct {
	int head,sect,cyl,wpcom,lzone,ctl;
	};
#ifdef HD_TYPE
struct hd_i_struct hd_info[] = { HD_TYPE };
#define NR_HD ((sizeof (hd_info))/(sizeof (struct hd_i_struct))) // 计算硬盘个数。
#else
struct hd_i_struct hd_info[] = { {0,0,0,0,0,0},{0,0,0,0,0,0} }; /* 硬盘信息数组 */
static int NR_HD = 0;   /* Linux0.11 中最大支持2块硬盘 */
#endif

// 定义硬盘分区结构。给出每个分区从硬盘0道开始算起的物理起始扇区号和分区扇区总数。
// 其中5 的倍数处的项（例如 ha[0]和 hd[5]等）代表整个硬盘的参数。
static struct hd_struct {
	long start_sect;    // 每个分区从硬盘0道开始算起的物理起始扇区号
	long nr_sects;      // 分区中扇区总数。
} hd[5*MAX_HD]={{0,0},};

// 读端口嵌入汇编宏。读端口 port，共读 nr 字，保存在 buf 中
#define port_read(port,buf,nr) \
__asm__("cld;rep;insw"::"d" (port),"D" (buf),"c" (nr))

// 写端口嵌入汇编宏。写端口 port，共写nr字，从 buf 中取数据。
#define port_write(port,buf,nr) \
__asm__("cld;rep;outsw"::"d" (port),"S" (buf),"c" (nr))

extern void hd_interrupt(void);
extern void rd_load(void);

/* This may be used only once, enforced by 'static int callable' */
// 读取硬盘參数（包括分区表信息）并建立虚拟盘和安装根文件系统设备。
int sys_setup(void * BIOS)
{
	static int callable = 1;
	int i,drive;
	unsigned char cmos_disks;
	struct partition *p;
	struct buffer_head * bh;

	// 用静态变量 callable 作为可调用标志。保证当前函数只运行一次
	if (!callable)
		return -1;
	callable = 0;  // 调用完后，将该变量设置为0，后续该函数不会再执行。

#ifndef HD_TYPE
	// 根据setup.s 程序获取的 BIOS 硬盘参数表信息，设置硬盘信息hd_info
	// 参考表9-10 硬盘基本参数信息表
	for (drive=0 ; drive<2 ; drive++) {
		hd_info[drive].cyl = *(unsigned short *) BIOS;  // unsigned short: 2字节（1个字）
		hd_info[drive].head = *(unsigned char *) (2+BIOS);
		hd_info[drive].wpcom = *(unsigned short *) (5+BIOS);
		hd_info[drive].ctl = *(unsigned char *) (8+BIOS);
		hd_info[drive].lzone = *(unsigned short *) (12+BIOS);
		hd_info[drive].sect = *(unsigned char *) (14+BIOS);
		BIOS += 16;
	}
	// setup.s 程序在取 BIOS 硬盘参数表信息时，如果系统中只有1个硬盘，就会将对应第2个硬盘的16字节全部清零。
	// 因此这里只要判断第 2 个硬盘柱面数是否为 0 就可以知道是否有第2个硬盘了。
	if (hd_info[1].cyl)
		NR_HD=2;
	else
		NR_HD=1;
#endif
	//  设置分区信息(这里只设置了整盘的分区信息)
	for (i=0 ; i<NR_HD ; i++) {
		hd[i*5].start_sect = 0;
		hd[i*5].nr_sects = hd_info[i].head*
				hd_info[i].sect*hd_info[i].cyl;
	}

	/*
		We querry CMOS about hard disks : it could be that 
		we have a SCSI/ESDI/etc controller that is BIOS
		compatable with ST-506, and thus showing up in our
		BIOS table, but not register compatable, and therefore
		not present in CMOS.

		Furthurmore, we will assume that our ST-506 drives
		<if any> are the primary drives in the system, and 
		the ones reflected as drive 1 or 2.

		The first drive is stored in the high nibble of CMOS
		byte 0x12, the second in the low nibble.  This will be
		either a 4 bit drive type or 0xf indicating use byte 0x19 
		for an 8 bit type, drive 1, 0x1a for drive 2 in CMOS.

		Needless to say, a non-zero value means we have 
		an AT controller hard disk for that drive.

		
	*/

	// 这里根据上述原理，下面代码用来检测硬盘到底是不是 AT 控制器兼容的。
	if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)
		if (cmos_disks & 0x0f)
			NR_HD = 2;
		else
			NR_HD = 1;
	else
		NR_HD = 0;
	// 如果硬盘为非兼容AT控制器硬盘，则清除硬盘信息。
	//若 NR_HD = 0，则两个硬盘都不是 AT 控制器兼容的，两个硬盘数据结构全清零。
	//若 NR_HD = 1，则将第2个硬盘的参数清零。
	for (i = NR_HD ; i < 2 ; i++) {
		hd[i*5].start_sect = 0;
		hd[i*5].nr_sects = 0;
	}
	// 设置分区信息（从磁盘第1个扇区中的分区表中获取信息）
	for (drive=0 ; drive<NR_HD ; drive++) {
		if (!(bh = bread(0x300 + drive*5,0))) {
			printk("Unable to read partition table of drive %d\n\r",
				drive);
			panic("");
		}
		if (bh->b_data[510] != 0x55 || (unsigned char)
		    bh->b_data[511] != 0xAA) {
			printk("Bad partition table on drive %d\n\r",drive);
			panic("");
		}
		p = 0x1BE + (void *)bh->b_data; // 分区表位于硬盘第1扇区的0x1BE 处。
		// 设置分区信息
		for (i=1;i<5;i++,p++) {
			hd[i+5*drive].start_sect = p->start_sect;
			hd[i+5*drive].nr_sects = p->nr_sects;
		}
		brelse(bh);
	}
	if (NR_HD)
		printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
	rd_load();      // 尝试创建并加载虚拟盘。
	mount_root();   // 安装根文件系统。
	return (0);
}

// 等待硬盘控制器就绪。
// 如果返回值(retries)为0，则表示等待控制器空闲的时间已经超时而发生错误；
// 如果返回值(retries)不为0，则说明在等待（循环）时间期限内控制器回到空闲状态，OK！
static int controller_ready(void)
{
	int retries=100000;

	// 参照：表9-6 8位主状态寄存器
	// 循环retries次，检测状态寄存器忙位（位7）是否为1来判断控制器是否处于忙状态。如果为0，表示没有处于忙状态，即控制台就绪
	while (--retries && (inb_p(HD_STATUS)&0x80));
	return (retries);
}

// 检测硬盘执行命令后的状态。（win 表示温切斯特硬盘的缩写）
// 返回值0 - 状态正常
// 返回值1 - 状态异常
static int win_result(void)
{
	int i=inb_p(HD_STATUS);

	// 参照：表9-6 8位主状态寄存器
	// 如果主状态寄存器中要求的5个bit位中，只有READY_STAT或SEEK_STAT置位，则表示状态正常。
	if ((i & (BUSY_STAT | READY_STAT | WRERR_STAT | SEEK_STAT | ERR_STAT))
		== (READY_STAT | SEEK_STAT))
		return(0); /* ok */
	// 如果 ERR_STAT 置位，则读取错误寄存器中的值（取出值未使用？）。
	if (i&1) i=inb(HD_ERROR);
	return (1);
}

// 向硬盘控制器发送命令块
// 硬盘控制器收到命令块后，就会执行命令。当命令执行完后，会产生一个硬盘中断。
static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
		unsigned int head,unsigned int cyl,unsigned int cmd,
		void (*intr_addr)(void))
{
	// 定义了一个寄存器变量 port 。该变量将被保存在一个寄存器中，以便于快速访问和操作
	register int port asm("dx");

	if (drive>1 || head>15)
		panic("Trying to write bad sector");
	if (!controller_ready())
		panic("HD controller not ready");
	// 设置硬盘中断发生时将调用的C函数指针 do_hd
	do_hd = intr_addr;
	// 将磁盘的控制字节写入磁盘控制器的控制寄存器
	outb_p(hd_info[drive].ctl,HD_CMD);

	// 不管什么命令均需要完整输出这 7 字节的命令块：首先发送 6 字节的参数，最后发出 1 字节的命令码
	port=HD_DATA;
	// 向向端口写入写前预补偿（HD_PRECOMP）
	outb_p(hd_info[drive].wpcom>>2,++port);
	// HD_NSECTOR
	outb_p(nsect,++port);
	// HD_SECTOR
	outb_p(sect,++port);
	// HD_LCYL
	outb_p(cyl,++port);
	// HD_HCYL
	outb_p(cyl>>8,++port);
	// 向端口写入驱动器号和磁头号（HD_CURRENT）
	outb_p(0xA0|(drive<<4)|head,++port);
	// HD_COMMAND
	outb(cmd,++port);
}

// 等待驱动器就绪
static int drive_busy(void)
{
	unsigned int i;

	// 循环等待主状态控制器的 就绪标志置位且忙标志位复位 或 超时
	for (i = 0; i < 10000; i++)
		// 如果就绪标志置位，且忙标志位复位
		if (READY_STAT == (inb_p(HD_STATUS) & (BUSY_STAT|READY_STAT)))
			break;
	i = inb(HD_STATUS);
	i &= BUSY_STAT | READY_STAT | SEEK_STAT;
	// 若仅有就绪或寻道结束标志置位（且忙标志位复位），则表示硬盘就绪，成功返回0
	if (i == (READY_STAT | SEEK_STAT))
		return(0);
	printk("HD controller times out\n\r");
	// 若经过一段时间仍为忙，则返回1。
	return(1);
}

// 复位硬盘控制器（复位硬盘控制寄存器）
// 实现：通过向端口HD_CMD（硬盘控制寄存器）发送控制字节实现
static void reset_controller(void)
{
	int	i;

	// 发送允许复位（4，位2置位）控制字节（只是允许，但是还没有复位）
	// 表9-8 硬盘控制字节的含义
	outb(4,HD_CMD);
	for(i = 0; i < 100; i++) nop();
	// 发送正常控制字节（不禁止重试、重读）
	outb(hd_info[0].ctl & 0x0f ,HD_CMD);
	if (drive_busy())
		printk("HD-controller still busy\n\r");
	if ((i = inb(HD_ERROR)) != 1)
		printk("HD-controller reset failed: %02x\n\r",i);
}

// 复位硬盘 ，参数nr为0或1
	// 首先复位 硬盘控制器。
	// 然后建立驱动器参数（通过发送硬盘控制器命令 WIN_SPECIFY）
static void reset_hd(int nr)
{
	reset_controller();
	hd_out(nr,hd_info[nr].sect,hd_info[nr].sect,hd_info[nr].head-1,
		hd_info[nr].cyl,WIN_SPECIFY,&recal_intr);  /* 函数 recal_intr(）是在硬盘中断处理程序中调用的 重新校正处理函数 */
}

void unexpected_hd_interrupt(void)
{
	printk("Unexpected HD interrupt\n\r");
}

// 读写硬盘失败处理操作（intr：Interupt Request）
// 统计出错次数，并复位硬盘继续处理本请求 或 结束本次请求项继续处理下一个请求项
static void bad_rw_intr(void)
{
	// 若出错次数不到最大允许出错次数的一半，则会先执行硬盘复位操作，然后再执行本次请求项处理。
	// 若出错次数己经大于等于最大允许出错次数 MAX_ERRORS （7次），则结束本次请求项的处理而去处理队列中下一个请求项。
	if (++CURRENT->errors >= MAX_ERRORS)
		end_request(0);
	if (CURRENT->errors > MAX_ERRORS/2)
		reset = 1;
}

// 中断函数1：读操作中断调用函数。
// 该函数将在硬盘读命令结束时引发的硬盘中断过程中被调用。在读命令执行后会产生硬盘中断信号，并执行硬盘中断处理程序，此时在硬盘中断处理程序中调用的C函数指针 do_hd 已经指向 read_intr()，
// 因此会在一次读扇区操作完成（或出错） 后就会执行该函数。
static void read_intr(void)
{
	if (win_result()) {
		bad_rw_intr();   /* 进行读写硬盘失败处理 */
		do_hd_request(); /* 执行其他硬盘请求操作 */
		return;
	}
	// 如果读命令没有出错，则从数据寄存器端口把1个扇区的数据读到请求项的缓冲区中
	port_read(HD_DATA,CURRENT->buffer,256);
	CURRENT->errors = 0;   /* 错误计数清零 */
	CURRENT->buffer += 512;
	CURRENT->sector++;
	// 若递减后不等于 0，表示本项请求还有数据没取完，于是再次置中断调用C函数指针do_hd 已经指向 read_intr()并直接返回，
	// 等待硬盘在读出另1个扇区数据后发出中断并再次调用本函数。
	if (--CURRENT->nr_sectors) {
		do_hd = &read_intr;
		return;
	}
	// 执行到此，说明本次请求项的全部扇区数据已经读完，则调用 end_request() 函数去处理请求项结束事宜。
	// 最后再次调用 do_hd_request()，去处理其他硬盘请求项。执行其他硬盘请求操作。
	end_request(1);
	do_hd_request();
}

// 中断函数2：写扇区中断
static void write_intr(void)
{
	if (win_result()) {
		bad_rw_intr();
		do_hd_request();
		return;
	}
	if (--CURRENT->nr_sectors) {
		CURRENT->sector++;
		CURRENT->buffer += 512;
		do_hd = &write_intr;
		port_write(HD_DATA,CURRENT->buffer,256);
		return;
	}
	end_request(1);
	do_hd_request();
}

// 中断函数3： 硬盘重新校正（复位）中断调用函数
// 该函数会在硬盘执行重新校正操作而引发的硬盘中断中被调用。
static void recal_intr(void)
{
	if (win_result())
		bad_rw_intr();
	// 根据当时具体的标志状态来判别是否需要先执行复位、重新校正等操作，然后再继续或处理下一请求项。
	do_hd_request();
}

// 处理硬盘请求项
// 实现：
// 该函数根据设备当前请求项中的设备号和起始扇区号信息，首先计算得到对应硬盘上的柱面号、 当前磁道中扇区号、磁头号数据，
// 然后再根据请求项中的命令（READ/WRITE）对硬盘发送相应读/写命令。
// 若控制器复位标志或硬盘重新校正标志已被置位，那么在发送操作命令前，首先会去执行复位或重新校正操作。
void do_hd_request(void)
{
	int i,r = 0;
	unsigned int block,dev;
	unsigned int sec,head,cyl;
	unsigned int nsect;

	INIT_REQUEST;
	dev = MINOR(CURRENT->dev); /* 子设备号 */
	block = CURRENT->sector;  /* 请求的起始扇区 */
	// block+2中的2，是因为一次块读写操作2个扇区，要保证请求的一个块中的2个扇区不能超出分区大小(即块不能跨分区)
	if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {
		end_request(0);
		goto repeat;
	}
	// 请求的起始扇区号加上子设备号对应分区的起始扇区号，就把需要读写的块对应到整个硬盘的绝对扇区号 block 上。
	block += hd[dev].start_sect; /* 硬盘的绝对扇区号  */
	// 子设备号被 5 整除即可得到分区对应的硬盘号
	dev /= 5;
	// 根据求得的绝对扇区号 block 和硬盘号 dev，我们就可以计算出对应硬盘中的 当前磁道中扇区号（sec）、所在柱面号（cyl）和磁头号 （head)。
	__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
		"r" (hd_info[dev].sect));
	__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
		"r" (hd_info[dev].head));
	// CHS中，每个磁道上的扇区从1开始编号（当前磁道上的扇区号）。而绝对扇区号（LBA、逻辑块地址、一维的线性地址）是从0开始编号。
	sec++;
	nsect = CURRENT->nr_sectors;
	// 如果此时复位标志 reset 是置位的，则需要执行复位操作。复位硬盘和控制器
	if (reset) {
		reset = 0;
		// 复位操作，必须同时执行重新校正，将磁头移动到0柱面
		recalibrate = 1;
		reset_hd(CURRENT_DEV);  /* recal_intr 函数会重新调用本函数 */
		return;
	}
	if (recalibrate) {
		recalibrate = 0;
		hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0,
			WIN_RESTORE,&recal_intr);  /* recal_intr 函数会重新调用本函数 */
		return;
	}	
	// 处理写扇区
	if (CURRENT->cmd == WRITE) {
		// 向硬盘控制器发送命令块（写命令WIN_WRITE）
		hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
		// 循环读取状态寄存器信息并判断驱动器是否已经准备好在主机和数据端口之间传输一个字或一个字节的数据
		for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)
			/* nothing */ ;
		if (!r) {
			bad_rw_intr();
			goto repeat;
		}
		// 向端口写入1个扇区的数据，写操作完成后，硬盘控制器会向CPU发送一个硬盘中断
		port_write(HD_DATA,CURRENT->buffer,256);
	// 处理读扇区
	} else if (CURRENT->cmd == READ) {
		hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
	} else
		panic("unknown hd-command");
}

void hd_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
	set_intr_gate(0x2E,&hd_interrupt);  /* int46 ，硬盘中断 */
	outb_p(inb_p(0x21)&0xfb,0x21);  // 复位接联的主8259A int2 的屏蔽位。
	outb(inb_p(0xA1)&0xbf,0xA1);    // 复位硬盘中断请求屏蔽位（在从片上）
}
